﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Xml.Linq;
using Grundy.Common;
using Grundy.CryptographicServices;
using Grundy.Entity;
using Grundy.Message;
using Grundy.Message.Administration;
using Grundy.Message.Licensing;

namespace Grundy.Client
{
    public class LicenseClient
    {
        private readonly string _customerKey;
        private readonly string _customerName;
        private readonly Guid _customerId;
        private static RNGCryptoServiceProvider _nonceGenerator = new RNGCryptoServiceProvider();

        private string GetNonce()
        {
            byte[] bytes = new byte[8];
            _nonceGenerator.GetBytes(bytes);
            return BitConverter.ToUInt64(bytes, 0).ToString();
        }

        private class AsyncState
        {
            public AsyncState(TcpClient client, ManualResetEvent manualResetEvent, FeatureIdentificationKey featureIdentificationKey, Request request)
            {
                Client = client;
                ManualResetEvent = manualResetEvent;
                FeatureIdentificationKey = featureIdentificationKey;
                Request = request;
            }

            public TcpClient Client { get; set; }
            public ManualResetEvent ManualResetEvent { get; set; }
            public FeatureIdentificationKey FeatureIdentificationKey { get; set; }
            public Request Request { get; set; }
            public Response Response { get; set; }
        }

        private readonly ClientConfiguration _clientConfiguration;

        public LicenseClient(ClientConfiguration clientConfiguration, string customerKey, string customerName = null, Guid customerId = default(Guid))
        {
            if (customerName.IsNullOrEmpty() && customerId == default(Guid))
                throw new ArgumentException("At least one of customer id or customer name is required for the license client.");

            _clientConfiguration = clientConfiguration;
            _customerKey = customerKey;
            _customerName = customerName;
            _customerId = customerId;

            GetClientCryptographicServicesProvider();
        }

        private Sha256MacProvider GetClientCryptographicServicesProvider()
        {
            return new Sha256MacProvider(_customerKey.ToByteArray(), _customerId, _customerName);
        }
        
        public virtual LicenseResponseBase Checkout(FeatureIdentificationKey featureIdentificationKey)
        {
            // Handle local licenses, only match
            var fikMatcher = new FeatureIdentificationKeyMatcher(featureIdentificationKey, new FeatureIdentificationKeyMatchScoreEvaluator());
            var cpleFikTuples = _clientConfiguration.NodeLockedEntitlements.Select(i => Tuple.Create(i, i.ToFeatureIdentificationKey()));
            var matchingKeys = fikMatcher.FindMatchingKeys(cpleFikTuples.Select(i => i.Item2));

            if (matchingKeys.Count == 1)
            {
                // Get the matching cple matched fik. Use it to check for validity
                var lev = new LicenseEntitlementValidity(cpleFikTuples.First(i => i.Item2.Equals(matchingKeys.First())).Item1);
                if (!lev.IsValid())
                {
                    throw new LicenseNotBegunOrExpiredException(lev);
                }
                return new LocalLicenseResponse {FeatureIdentificationKey = matchingKeys.First(), IsSuccessful = true};
            }
            if (matchingKeys.Count > 1)
            {
                throw new AmbiguousLicenseException(cpleFikTuples.Where(i => matchingKeys.Contains(i.Item2)).Select(i => i.Item1));
            }

            // Handle floating licenses
            var challenge = GetNonce();
            var checkoutRequest = new CheckoutRequest { Identifiers = _clientConfiguration.ApiIdentity, FeatureIdentificationKey = featureIdentificationKey, Challenge = challenge };
            var client = new TcpClient();
            var completed = new ManualResetEvent(false);
            var state = new AsyncState(client, completed, featureIdentificationKey, checkoutRequest);
            client.BeginConnect(_clientConfiguration.LicenseServerAddress,
                                _clientConfiguration.LicenseServerPort, Connected, 
                                state);
            state.ManualResetEvent.WaitOne();
            var response = state.Response;
            if(response == null)
            {
                return new RemoteLicenseResponse{IsSuccessful = false, Message = "Container could not be verified.", FeatureIdentificationKey = featureIdentificationKey};
            }
            if (response is Error)
            {
                var error = (Error) response;
                return new RemoteLicenseResponse{IsSuccessful = false, Message = error.Message, Details = error.Details, FeatureIdentificationKey = featureIdentificationKey};
            }
            if(response is CheckoutSucceededResponse)
            {
                var success = (CheckoutSucceededResponse) response;
                if (success.Challenge != challenge)
                {
                    return new RemoteLicenseResponse { IsSuccessful = false, Message = "Authenticity of the response could not be verified. Expected challenge {0} but recieved {1}".FormatWith(challenge, success.Challenge), FeatureIdentificationKey = featureIdentificationKey };
                }
                return new RemoteLicenseResponse{IsSuccessful = true, FeatureIdentificationKey = featureIdentificationKey, SessionIdentifier = success.SessionId};
            }
            throw new InvalidResponseException(response);
        }
        
        public virtual LicenseResponseBase Checkin(LicenseResponseBase previousCheckoutResponse)
        {
            if (!previousCheckoutResponse.IsSuccessful)
            {
                throw new InvalidOperationException("The previous license response was not successful.");
            }

            if (previousCheckoutResponse is LocalLicenseResponse)
            {
                return new LocalLicenseResponse {FeatureIdentificationKey = previousCheckoutResponse.FeatureIdentificationKey, IsSuccessful = true};
            }

            var remoteCheckoutResponse = (RemoteLicenseResponse)previousCheckoutResponse;
            if (remoteCheckoutResponse.SessionIdentifier == Guid.Empty)
            {
                throw new InvalidOperationException("Cannot checkin the license as the session identifier is empty.");
            }
            var challenge = GetNonce();
            var checkinRequest = new CheckinRequest { Challenge = challenge, SessionId = remoteCheckoutResponse.SessionIdentifier };
            var client = new TcpClient();
            var completed = new ManualResetEvent(false);
            var state = new AsyncState(client, completed, FeatureIdentificationKey.Null, checkinRequest);
            client.BeginConnect(_clientConfiguration.LicenseServerAddress,
                                _clientConfiguration.LicenseServerPort, Connected,
                                state);
            state.ManualResetEvent.WaitOne();
            var response = state.Response;
            var fik = previousCheckoutResponse.FeatureIdentificationKey;

            if (response == null)
            {
                return new RemoteLicenseResponse { IsSuccessful = false, Message = "Container could not be verified.", FeatureIdentificationKey = fik };
            }
            if (response is Error)
            {
                var error = (Error)response;
                return new RemoteLicenseResponse { IsSuccessful = false, Message = error.Message, Details = error.Details, FeatureIdentificationKey = fik };
            }
            if (response is CheckinSucceededResponse)
            {
                var success = (CheckinSucceededResponse)response;
                if (success.Challenge != challenge)
                {
                    return new RemoteLicenseResponse { IsSuccessful = false, Message = "Authenticity of the response could not be verified. Expected challenge {0} but recieved {1}".FormatWith(challenge, success.Challenge), FeatureIdentificationKey = fik };
                }
                return new RemoteLicenseResponse { IsSuccessful = true, FeatureIdentificationKey = fik };
            }
            throw new InvalidResponseException(response);
        }

        public virtual Response ProcessAdministrationRequest(AdministrationRequest request)
        {   
            var client = new TcpClient();
            var completed = new ManualResetEvent(false);
            var state = new AsyncState(client, completed, FeatureIdentificationKey.Null, request);
            client.BeginConnect(_clientConfiguration.LicenseServerAddress,
                                _clientConfiguration.LicenseServerPort, Connected,
                                state);
            state.ManualResetEvent.WaitOne();
            var response = state.Response;
            return response;
        }
        
        private void Connected(IAsyncResult ar)
        {
            var state = (AsyncState)ar.AsyncState;
            var client = state.Client;
            client.EndConnect(ar);
            var licenseResponse = Negotiate(state.Client, state.Request);
            state.Response = licenseResponse;
            state.ManualResetEvent.Set();
        }

        private class ReadAsyncState
        {
            public int TotalBytesRead;
            public int BytesRead;
            public NetworkStream NetworkStream;
            public ManualResetEvent ManualResetEvent = new ManualResetEvent(false);
        }

        private Response Negotiate(TcpClient client, Request request)
        {
            var networkStream = client.GetStream();
            networkStream.ReadTimeout = 10000;
            networkStream.WriteTimeout = 10000;


            var container = GetClientCryptographicServicesProvider().Sign(request, ContainerSigneeType.Client);
            var outMessage = container.Serialize().ToByteArrayUsingUtf8();
            var outBuffer = new byte[outMessage.Length + 1];
            Array.Copy(outMessage, outBuffer, outMessage.Length);
            outBuffer[outMessage.Length] = 4;
            networkStream.Write(outBuffer, 0, outBuffer.Length);
            
            var bufferSize = _clientConfiguration.MaximumMessageSize;
            var inBuffer = new byte[bufferSize];
            var rs = new ReadAsyncState { NetworkStream = networkStream };
            do
            {
                rs.ManualResetEvent.Reset();
                networkStream.BeginRead(inBuffer, rs.TotalBytesRead, bufferSize, DataReceived, rs);
                rs.ManualResetEvent.WaitOne();
            } while (rs.TotalBytesRead <= bufferSize && rs.BytesRead > 0 && inBuffer[rs.TotalBytesRead - 1] != 4);
            
            var responseString = UTF8Encoding.UTF8.GetString(inBuffer, 0, rs.TotalBytesRead - 1);
            var responseContainer = responseString.Deserialize<Container>();
            var response = responseContainer.VerifyPayload<Response>(_clientConfiguration.EntitlementServerCryptographicServicesProvider, _clientConfiguration.ServerCryptographicServicesProvider, null);
            
            return response;
        }

        private void DataReceived(IAsyncResult ar)
        {
            var rs = (ReadAsyncState) ar.AsyncState;
            var bytesRead = rs.NetworkStream.EndRead(ar);
            rs.BytesRead = bytesRead;
            rs.TotalBytesRead += rs.BytesRead;
            rs.ManualResetEvent.Set();
        }
    }
}
